package org.sample.springbootredis.lock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RedisLock {

	private static final Logger LOGGER = LoggerFactory.getLogger(RedisLock.class);
	
	/**
	 * By default, a thread should not hold a lock over 30 seconds.
	 */
	private static final int DEFAULT_EXPIRE_MILLIS = 30000;
	
	/**
	 * By default, a thread retry to acquire lock again after 1 second.
	 */
	private static final int DEFAULT_RETRY_MILLIS = 1000;
	
	/**
	 * By default, a thread block 30 seconds to try acquiring lock.
	 */
	private static final int DEFAULT_TIMEOUT_MILLIS = 30000;

	private LockRepository lockRepository;
	
	private LockKey lockKey;
	
	private String additionalKey;
	
	private int expireMillis;
	
	private int retryMillis;

	private String lockedValue;

	public RedisLock(LockRepository mbTokenLockRepository, LockKey lockKey, String additionalKey,
			int expireMillis, int retryMillis) {

		this.lockRepository = mbTokenLockRepository;
		this.lockKey = lockKey;
		this.additionalKey = additionalKey;
		this.expireMillis = expireMillis;
		this.retryMillis = retryMillis;
	}

	public RedisLock(LockRepository mbTokenLockRepository, LockKey lockKey, String additionalKey) {

		this(mbTokenLockRepository, lockKey, additionalKey, DEFAULT_EXPIRE_MILLIS, DEFAULT_RETRY_MILLIS);
	}
	
	/**
	 * Try to acquire lock once.
	 * 
	 * @return true if lock is acquired, otherwise false.
	 */
	public boolean tryLock() {
		
		if (lockedValue != null) {
			throw new UnsupportedOperationException("It has been locked.");
		}
		
		this.lockedValue = this.lockRepository.lock(lockKey, additionalKey, expireMillis);
		return lockedValue != null;
	}

	/**
	 * Try to acquire lock, and retry during specified time interval.
	 * 
	 * @param timeoutMillis maximum milliseconds of waiting for acquiring lock.
	 * @return  true if lock is acquired, otherwise false.
	 * @throws InterruptedException
	 */
	public boolean tryLock(int timeoutMillis) throws IllegalStateException {
		
		long endTimeMillis = System.currentTimeMillis() + timeoutMillis;
		
		while (!tryLock()) {
			if (endTimeMillis < System.currentTimeMillis()) {
				LOGGER.warn("Thread cannot get lock before timeout.");
				return false;
			}
			
			try {
				LOGGER.info("Block thread " + retryMillis + " milliseconds to retry acquire lock");
				Thread.sleep(retryMillis);
			} catch (InterruptedException e) {
				throw new IllegalStateException("Thread is interrupted when wait for lock", e);
			}
		}
		
		return true;
	}
	
	/**
	 * Keep acquiring lock until acquired, fail if not acquired before timeout.
	 * 
	 * @throws IllegalStateException
	 */
	public void lock() throws IllegalStateException {
		
		if (!tryLock(DEFAULT_TIMEOUT_MILLIS)) {
			throw new IllegalStateException("Thread cannot get lock");
		}
	}

	/**
	 * Release acquired lock.
	 */
	public void unlock() {
		
		if (lockedValue == null) {
			throw new UnsupportedOperationException("It has been unlocked or not been locked yet.");
		}

		this.lockRepository.unlock(lockKey, additionalKey, lockedValue);
		lockedValue = null;
	}
	
}
